// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using iPanel.Otlp.Model.MetricValues;
using Google.Protobuf.Collections;
using OpenTelemetry.Proto.Common.V1;
using JsonIgnoreAttribute = Newtonsoft.Json.JsonIgnoreAttribute;

namespace iPanel.Otlp.Model;

[DebuggerDisplay("Name = {Name}, Unit = {Unit}, Type = {Type}")]
public class OtlpInstrumentSummary
{
    public required string Name { get; set; }
    public string NameLocal { get; set; }
    public required string Description { get; init; }
    public required string Unit { get; init; }
    public required OtlpInstrumentType Type { get; init; }
    public required OtlpMeter Parent { get; init; }

    public OtlpInstrumentKey GetKey() => new(Parent.MeterName, Name);
}

public class OtlpInstrumentData
{
    public required OtlpInstrumentSummary Summary { get; init; }
    public required List<DimensionScope> Dimensions { get; init; }
    public required Dictionary<string, List<string?>> KnownAttributeValues { get; init; }
    public required bool HasOverflow { get; init; }
}

[DebuggerDisplay("Name = {Summary.Name}, Unit = {Summary.Unit}, Type = {Summary.Type}")]
public class OtlpInstrument
{
    public required OtlpInstrumentSummary Summary { get; init; }

    [JsonIgnore]
    public required OtlpContext Context { get; init; }

    public Dictionary<ReadOnlyMemory<KeyValuePair<string, string>>, DimensionScope> Dimensions { get; } = new(ScopeAttributesComparer.Instance);
    public Dictionary<string, List<string?>> KnownAttributeValues { get; } = new();
    public bool HasOverflow { get; set; }

    public DimensionScope FindScope(RepeatedField<KeyValue> attributes, ref KeyValuePair<string, string>[]? tempAttributes)
    {
        // See https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#overflow-attribute
        // Inspect attributes before they're merged with parent attributes. "otel.metric.overflow" should be the only attribute.
        if (!HasOverflow && attributes.Count == 1 && attributes[0].Key == "otel.metric.overflow" && attributes[0].Value.GetString() == "true")
        {
            HasOverflow = true;
        }

        // We want to find the dimension scope that matches the attributes, but we don't want to allocate.
        // Copy values to a temporary reusable array.
        //
        // A meter can have attributes. Merge these with the data point attributes when creating a dimension.
        OtlpHelpers.CopyKeyValuePairs(attributes, Summary.Parent.Attributes, Context, out var copyCount, ref tempAttributes);
        Array.Sort(tempAttributes, 0, copyCount, KeyValuePairComparer.Instance);

        var comparableAttributes = tempAttributes.AsMemory(0, copyCount);

        // Can't use CollectionsMarshal.GetValueRefOrAddDefault here because comparableAttributes is a view over mutable data.
        // Need to add dimensions using durable attributes instance after scope is created.
        if (!Dimensions.TryGetValue(comparableAttributes, out var dimension))
        {
            dimension = CreateDimensionScope(comparableAttributes);
            Dimensions.Add(dimension.Attributes, dimension);
        }
        return dimension;
    }

    private DimensionScope CreateDimensionScope(Memory<KeyValuePair<string, string>> comparableAttributes)
    {
        var isFirst = Dimensions.Count == 0;
        var durableAttributes = comparableAttributes.ToArray();
        var dimension = new DimensionScope(Context.Options.MaxMetricsCount, durableAttributes);

        var keys = KnownAttributeValues.Keys.Union(durableAttributes.Select(a => a.Key)).Distinct();
        foreach (var key in keys)
        {
            ref var values = ref CollectionsMarshal.GetValueRefOrAddDefault(KnownAttributeValues, key, out _);
            // Adds to dictionary if not present.
            if (values == null)
            {
                values = new List<string?>();

                // If the key is new and there are already dimensions, add an empty value because there are dimensions without this key.
                if (!isFirst)
                {
                    TryAddValue(values, null);
                }
            }

            var currentDimensionValue = OtlpHelpers.GetValue(durableAttributes, key);
            TryAddValue(values, currentDimensionValue);
        }

        return dimension;

        static void TryAddValue(List<string?> values, string? value)
        {
            if (!values.Contains(value))
            {
                values.Add(value);
            }
        }
    }

    public static OtlpInstrument Clone(OtlpInstrument instrument, bool cloneData, DateTime? valuesStart, DateTime? valuesEnd)
    {
        var newInstrument = new OtlpInstrument
        {
            Summary = instrument.Summary,
            Context = instrument.Context,
            HasOverflow = instrument.HasOverflow
        };

        if (cloneData)
        {
            foreach (var item in instrument.KnownAttributeValues)
            {
                newInstrument.KnownAttributeValues.Add(item.Key, item.Value.ToList());
            }
            foreach (var item in instrument.Dimensions)
            {
                newInstrument.Dimensions.Add(item.Key, DimensionScope.Clone(item.Value, valuesStart, valuesEnd));
            }
        }

        return newInstrument;
    }

    private sealed class ScopeAttributesComparer : IEqualityComparer<ReadOnlyMemory<KeyValuePair<string, string>>>
    {
        public static readonly ScopeAttributesComparer Instance = new();

        public bool Equals(ReadOnlyMemory<KeyValuePair<string, string>> x, ReadOnlyMemory<KeyValuePair<string, string>> y)
        {
            return x.Span.SequenceEqual(y.Span);
        }

        public int GetHashCode([DisallowNull] ReadOnlyMemory<KeyValuePair<string, string>> obj)
        {
            var hashcode = new HashCode();
            foreach (KeyValuePair<string, string> pair in obj.Span)
            {
                hashcode.Add(pair.Key);
                hashcode.Add(pair.Value);
            }
            return hashcode.ToHashCode();
        }
    }

    private sealed class KeyValuePairComparer : IComparer<KeyValuePair<string, string>>
    {
        public static readonly KeyValuePairComparer Instance = new();

        public int Compare(KeyValuePair<string, string> x, KeyValuePair<string, string> y)
        {
            return string.Compare(x.Key, y.Key, StringComparison.Ordinal);
        }
    }
}
